home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / python-support / gnome-games-data / glchess / ai.py < prev    next >
Encoding:
Python Source  |  2009-04-14  |  13.4 KB  |  469 lines

  1. # -*- coding: utf-8 -*-
  2. __author__ = 'Robert Ancell <bob27@users.sourceforge.net>'
  3. __license__ = 'GNU General Public License Version 2'
  4. __copyright__ = 'Copyright 2005-2006  Robert Ancell'
  5.  
  6. import os
  7. import sys
  8. import select
  9. import signal
  10. import xml.dom.minidom
  11. import xml.parsers.expat
  12.  
  13. import game
  14. import cecp
  15. import uci
  16.  
  17. from defaults import *
  18.  
  19. CECP = 'CECP'
  20. UCI  = 'UCI'
  21.  
  22. class Option:
  23.     """
  24.     """
  25.     value = ''
  26.  
  27. class Level:
  28.     """
  29.     """
  30.     def __init__(self):
  31.         self.options = []
  32.  
  33. class Profile:
  34.     """
  35.     """   
  36.     def __init__(self):
  37.         self.name = ''
  38.         self.protocol = ''
  39.         self.path = ''
  40.         self.executables = []
  41.         self.arguments = []
  42.         self.profiles = {}
  43.  
  44.     def detect(self):
  45.         """
  46.         """
  47.         try:
  48.             path = os.environ['PATH'].split(os.pathsep)
  49.         except KeyError:
  50.             path = []
  51.  
  52.         for directory in path:
  53.             for executable in self.executables:
  54.                 b = directory + os.sep + executable
  55.                 if os.path.isfile(b):
  56.                     self.path = b
  57.                     return
  58.         self.path = None
  59.         
  60. def _getXMLText(node):
  61.     """
  62.     """
  63.     if len(node.childNodes) == 0:
  64.         return ''
  65.     if len(node.childNodes) > 1 or node.childNodes[0].nodeType != node.TEXT_NODE:
  66.         raise ValueError
  67.     return node.childNodes[0].nodeValue
  68.  
  69. def _loadLevel(node):
  70.     """
  71.     """
  72.     level = Level()
  73.     n = node.getElementsByTagName('name')
  74.     if len(n) != 1:
  75.         return None
  76.     level.name = _getXMLText(n[0])
  77.     
  78.     for e in node.getElementsByTagName('option'):
  79.         option = Option()
  80.         option.value = _getXMLText(e)
  81.         try:
  82.             attribute = e.attributes['name']
  83.         except KeyError:
  84.             pass
  85.         else:
  86.             option.name = _getXMLText(attribute)
  87.         level.options.append(option)
  88.         
  89.     return level
  90.  
  91. def loadProfiles():
  92.     """
  93.     """
  94.     profiles = []
  95.     
  96.     fileNames = [os.path.expanduser(LOCAL_AI_CONFIG), os.path.join(BASE_DIR, 'ai.xml'), 'ai.xml']
  97.     document = None
  98.     for f in fileNames:
  99.         try:
  100.             document = xml.dom.minidom.parse(f)
  101.         except IOError:
  102.             pass
  103.         except xml.parsers.expat.ExpatError:
  104.             print 'AI configuration from %s is invalid, ignoring' % f
  105.         else:
  106.             #print 'Loading AI configuration from %s' % f
  107.             break
  108.     if document is None:
  109.         print 'WARNING: No AI configuration'
  110.         return profiles
  111.  
  112.     elements = document.getElementsByTagName('aiconfig')
  113.     if len(elements) == 0:
  114.         return profiles
  115.  
  116.     for e in elements:
  117.         for p in e.getElementsByTagName('ai'):
  118.             try:
  119.                 protocolName = p.attributes['type'].nodeValue
  120.             except KeyError:
  121.                 assert(False)
  122.             if protocolName == 'cecp':
  123.                 protocol = CECP
  124.             elif protocolName == 'uci':
  125.                 protocol = UCI
  126.             else:
  127.                 assert(False), 'Uknown AI type: %s' % repr(protocolName)
  128.             
  129.             n = p.getElementsByTagName('name')
  130.             assert(len(n) > 0)
  131.             name = _getXMLText(n[0])
  132.  
  133.             executables = []
  134.             n = p.getElementsByTagName('binary')
  135.             assert(len(n) > 0)
  136.             for x in n:
  137.                 executables.append(_getXMLText(x))
  138.  
  139.             arguments = []
  140.             for x in p.getElementsByTagName('argument'):
  141.                 arguments.append(_getXMLText(x))
  142.  
  143.             levels = {}
  144.             for x in p.getElementsByTagName('level'):
  145.                 level = _loadLevel(x)
  146.                 if level is not None:
  147.                     levels[level.name] = level
  148.  
  149.             profile = Profile()
  150.             profile.name        = name
  151.             profile.protocol    = protocol
  152.             profile.executables = executables
  153.             profile.arguments   = arguments
  154.             profile.levels      = levels
  155.             profiles.append(profile)
  156.     
  157.     return profiles
  158.  
  159. class CECPConnection(cecp.Connection):
  160.     """
  161.     """
  162.     def __init__(self, player):
  163.         """
  164.         """
  165.         self.player = player
  166.         cecp.Connection.__init__(self)
  167.         
  168.     def onOutgoingData(self, data):
  169.         """Called by cecp.Connection"""
  170.         self.player.logText(data, 'output')
  171.         self.player.sendToEngine(data)
  172.  
  173.     def onMove(self, move):
  174.         """Called by cecp.Connection"""
  175.         if self.player.isReadyToMove():
  176.             self.player.moving = True
  177.             self.player.move(move)
  178.         else:
  179.             assert(self.player.suppliedMove is None)
  180.             self.player.suppliedMove = move
  181.         
  182.     def logText(self, text, style):
  183.         """Called by cecp.Connection"""
  184.         self.player.logText(text, style)
  185.  
  186. class UCIConnection(uci.StateMachine):
  187.     """
  188.     """
  189.     def __init__(self, player):
  190.         """
  191.         """
  192.         self.player = player
  193.         uci.StateMachine.__init__(self)
  194.         
  195.     def onOutgoingData(self, data):
  196.         """Called by uci.StateMachine"""
  197.         self.player.logText(data, 'output')
  198.         self.player.sendToEngine(data)
  199.         
  200.     def logText(self, text, style):
  201.         """Called by uci.StateMachine"""
  202.         self.player.logText(text, style)
  203.  
  204.     def onMove(self, move):
  205.         """Called by uci.StateMachine"""
  206.         self.player.move(move)
  207.  
  208. # Catch zombie processes
  209. def _cDied(sig, stackFrame):
  210.     try:
  211.         (pid, status) = os.waitpid(-1, os.WNOHANG)
  212.     except OSError:
  213.         pass
  214. signal.signal(signal.SIGCHLD, _cDied)      
  215.  
  216. class Player(game.ChessPlayer):
  217.     """
  218.     """
  219.     def __init__(self, name, profile, level = 'normal'):
  220.         """Constructor for an AI player.
  221.         
  222.         'name' is the name of the player (string).
  223.         'profile' is the profile to use for the AI (Profile).
  224.         'level' is the difficulty level to use (string).
  225.         """
  226.         self.__profile = profile
  227.         self.__level = level
  228.         
  229.         self.moving = False
  230.         self.suppliedMove = None
  231.  
  232.         game.ChessPlayer.__init__(self, name)
  233.         
  234.         # Pipe to communicate to engine with
  235.         (toManagerOutput, toManagerInput) = os.pipe()
  236.         (fromManagerOutput, fromManagerInput) = os.pipe()
  237.         
  238.         # Store the file descripter for reading/writing
  239.         self.__toEngineFd = toManagerInput
  240.         self.__fromEngineFd = fromManagerOutput
  241.  
  242.         # Fork off a child process to manage the engine
  243.         try:
  244.             self.__pid = os.fork()
  245.         except OSError, e:
  246.             print 'Monitor failed to fork: %s' % e.message
  247.             os.close(toManagerInput)
  248.             os.close(toManagerOutput)
  249.             os.close(fromManagerInput)
  250.             os.close(fromManagerOutput)
  251.             self.__toEngineFd = None
  252.             self.__fromEngineFd = None
  253.         else:
  254.             if self.__pid == 0:
  255.                 os.close(toManagerInput)
  256.                 os.close(fromManagerOutput)
  257.                 self._runMonitor(fromManagerInput, toManagerOutput)
  258.                 os.close(toManagerOutput)
  259.                 os.close(fromManagerInput)
  260.                 os._exit(0)
  261.  
  262.             os.close(toManagerOutput)
  263.             os.close(fromManagerInput)
  264.  
  265.             if profile.protocol == CECP:
  266.                 self.connection = CECPConnection(self)
  267.             elif profile.protocol == UCI:
  268.                 self.connection = UCIConnection(self)
  269.             else:
  270.                 assert(False)
  271.             
  272.             self.connection.start()
  273.             self.connection.startGame()
  274.             try:
  275.                 level = self.__profile.levels[self.__level]
  276.             except KeyError:
  277.                 self.connection.configure()
  278.             else:
  279.                 self.connection.configure(level.options)
  280.  
  281.     # Methods to extend
  282.     
  283.     def logText(self, text, style):
  284.         """
  285.         """
  286.         pass
  287.         
  288.     # Public methods
  289.     
  290.     def getProfile(self):
  291.         """Get the AI profile this AI is using.
  292.         
  293.         Returns a 2-tuple containing the profile name (str) and the difficulty level (str).
  294.         """
  295.         return (self.__profile.name, self.__level)
  296.     
  297.     def fileno(self):
  298.         """Returns the file descriptor for communicating with the engine (integer)"""
  299.         return self.__fromEngineFd
  300.  
  301.     def read(self):
  302.         """Read and process data from the engine.
  303.         
  304.         This is non-blocking.
  305.         """       
  306.         while True:
  307.             # Connection was closed
  308.             if self.__fromEngineFd == None:
  309.                 return False
  310.  
  311.             # Check if data is available
  312.             (rlist, _, xlist) = select.select([self.__fromEngineFd], [], [self.__fromEngineFd], 0)
  313.             if (len(rlist) + len(xlist)) == 0:
  314.                 return True
  315.  
  316.             # Read a chunk and process
  317.             try:
  318.                 data = os.read(self.__fromEngineFd, 256)
  319.             except OSError, e:
  320.                 print 'Error reading from chess engine: ' + str(e)
  321.                 self._die()
  322.                 return False
  323.             if len(data) == 0:
  324.                 print 'Engine has died'
  325.                 self._die()
  326.                 return False
  327.             self.connection.registerIncomingData(data)
  328.  
  329.     def sendToEngine(self, data):
  330.         """
  331.         """
  332.         if self.__toEngineFd == None:
  333.             return
  334.         
  335.         try:
  336.             os.write(self.__toEngineFd, data)
  337.         except OSError, e:
  338.             print 'Failed to write to engine: ' + str(e)
  339.  
  340.     def quit(self):
  341.         """Disconnect the AI"""
  342.         fd = self.__toEngineFd
  343.         self.__toEngineFd = None
  344.         self.__fromEngineFd = None
  345.         
  346.         # Send quit
  347.         try:
  348.             if fd is not None:
  349.                 os.write(fd, '\nquit\n') # FIXME: CECP specific
  350.         except OSError:
  351.             return
  352.  
  353.     # Extended methods
  354.     
  355.     def onPlayerMoved(self, player, move):
  356.         """Called by game.ChessPlayer"""
  357.         if self.__toEngineFd == None:
  358.             return
  359.         isSelf = player is self and self.moving
  360.         self.moving = False
  361.         self.connection.reportMove(move.canMove, isSelf)
  362.         
  363.     def onUndoMove(self):
  364.         """Called by game.ChessPlayer"""
  365.         self.connection.undoMove()
  366.  
  367.     def readyToMove(self):
  368.         """Called by game.ChessPlayer"""
  369.         if self.__toEngineFd == None:
  370.             self.die()
  371.             return
  372.         game = self.getGame()
  373.         whiteTime = game.getWhite().getRemainingTime()
  374.         blackTime = game.getBlack().getRemainingTime()
  375.         if game.getWhite() is self:
  376.             ownTime = whiteTime
  377.         else:
  378.             ownTime = blackTime
  379.         
  380.         if self.suppliedMove is None:
  381.             self.connection.requestMove(whiteTime, blackTime, ownTime)
  382.         else:
  383.             self.moving = True
  384.             move = self.suppliedMove
  385.             self.suppliedMove = None
  386.             self.move(move)
  387.         
  388.     def onGameEnded(self, game):
  389.         """Called by game.ChessPlayer"""
  390.         self.quit()
  391.         
  392.     def _die(self):
  393.         self.quit()
  394.         self.die()
  395.  
  396.     def _runEngine(self, toEngineFd, fromEngineFd):
  397.         # Make the engine low priority for CPU usage
  398.         os.nice(19)
  399.                 
  400.         # Change directory so any log files are not in the users home directory
  401.         try:
  402.             os.mkdir(LOG_DIR)
  403.         except OSError:
  404.             pass
  405.         try:
  406.             os.chdir(LOG_DIR)
  407.         except OSError:
  408.             pass
  409.                 
  410.         # Connect stdin, stdout and stderr to the manager process
  411.         os.dup2(toEngineFd, sys.stdin.fileno())
  412.         os.dup2(fromEngineFd, sys.stdout.fileno())
  413.         os.dup2(fromEngineFd, sys.stderr.fileno())
  414.                 
  415.         # Execute the engine
  416.         try:
  417.             os.execv(self.__profile.path, [self.__profile.path] + self.__profile.arguments)
  418.         except OSError:
  419.             pass
  420.  
  421.     def _runMonitor(self, toApplicationFd, fromApplicationFd):
  422.         # Make pipes to the child process
  423.         (toEngineOutput, toEngineInput) = os.pipe()
  424.         (fromEngineOutput, fromEngineInput) = os.pipe()
  425.  
  426.         # Fork and execute the child
  427.         try:
  428.             enginePID = os.fork()
  429.         except OSError, e:
  430.             print 'Monitor failed to fork: %s' % e.message
  431.             os._exit(1);
  432.         if enginePID == 0:
  433.             os.close(toApplicationFd)
  434.             os.close(fromApplicationFd)            
  435.             os.close(toEngineInput)
  436.             os.close(fromEngineOutput)
  437.             self._runEngine(toEngineOutput, fromEngineInput)
  438.             os._exit(0)
  439.         else:
  440.             os.close(toEngineOutput)
  441.             os.close(fromEngineInput)
  442.  
  443.         # Forward data between the application and the child process and wait for closed pipes
  444.         inputPipes = (fromApplicationFd, fromEngineOutput)
  445.         targets = {fromApplicationFd: toEngineInput,
  446.                    fromEngineOutput: toApplicationFd}
  447.         pipes = (toApplicationFd, fromApplicationFd,
  448.                  toEngineInput, fromEngineOutput)
  449.                  
  450.         try:
  451.             while True:
  452.                 # Wait for data
  453.                 (rfds, _, xfds) = select.select(inputPipes, [], pipes, None)
  454.                 
  455.                 for fd in rfds:
  456.                     data = os.read(fd, 65535)
  457.                     if len(data) == 0:
  458.                         raise OSError('End of data')
  459.                 
  460.                     # Bridge information between the application and the engine
  461.                     os.write(targets[fd], data)
  462.         except:
  463.             # Kill the child and exit
  464.             try:
  465.                 os.kill(enginePID, signal.SIGQUIT)
  466.             except OSError:
  467.                 pass
  468.             os._exit(0)
  469.